// Tencent is pleased to support the open source community by making tRPC available.
//
// Copyright (C) 2023 THL A29 Limited, a Tencent company.
// All rights reserved.
//
// If you have downloaded a copy of the tRPC source code from Tencent,
// please note that tRPC source code is licensed under the  Apache 2.0 License,
// A copy of the Apache 2.0 License is included in this file.

// Package fb encapsulates functions related to .fbs files.
package fb

import (
	"fmt"
	"os"

	"os/exec"
	"path"
	"path/filepath"
	"strings"

	"trpc.group/trpc-go/trpc-cmdline/util/log"
)

// Fbs stores a set of parameters that may be used when invoking flatc.
type Fbs struct {
	// language indicates the language information such as Go, etc.
	language string

	// The packagePath parameter only contains the last part of the namespace or go_package, for example,
	// if the namespace is trpc.testapp.testserver, then the packagePath is testserver.
	// This parameter is passed directly to flatc's --go-namespace option to ensure that the beginning of the
	// generated file package <pkg> is correct.
	packagePath string

	// fb2ImportPath maps flatbuffers file name to import path
	// for example "./file1.fbs" => "trpc.group/testapp/testserver1"
	fb2ImportPath map[string]string

	// Maps package names to their corresponding import paths.
	// For example, "trpc.testapp.testserver1" => "trpc.group/testapp/testserver1".
	pkg2ImportPath map[string]string

	args struct {
		// fbsfile represents the name of the flatbuffers file.
		// This field is initially passed in by the user from the trpc command line and may contain part of the path
		// information, such as "./test/file1.fbs".
		fbsfile string
		// includePaths indicates the paths to search for include files.
		includePaths []string
		// outDir represents the output directory.
		outDir string
	}
}

// NewFbs creates a new Fbs struct based on the input parameter list.
func NewFbs(opts ...Option) *Fbs {
	f := &Fbs{}
	for _, opt := range opts {
		opt(f)
	}
	// In the output path, the packagePath needs to be removed, because the packagePath is specified as --go-namespace.
	// In this way, flatc will create the packagePath folder in outDir when generating stub code.
	f.args.outDir = strings.TrimSuffix(f.args.outDir, fmt.Sprintf("%c%s", filepath.Separator, f.packagePath))
	return f
}

// Flatc executes flatc to generate stub code for each type defined in the .fbs file.
func (f *Fbs) Flatc() error {
	var args []string
	for _, dir := range f.args.includePaths {
		args = append(args, "-I")
		args = append(args, dir)
	}

	args = append(args, fmt.Sprintf("--%v", f.language)) // Indicates the language of the generated code passed to flatc.

	// Specify the namespace explicitly
	// Ensure that packagePath only contains the last segment, otherwise flatc will generate nested paths.
	args = append(args, "--go-namespace", f.packagePath)

	// Specifies the path where the generated files will be placed.
	args = append(args, "-o", f.args.outDir)

	// Specifies the flatbuffers file to process.
	args = append(args, f.args.fbsfile)

	cmd := exec.Command("flatc", args...)
	s := strings.Join(cmd.Args, " ")

	if buf, err := cmd.CombinedOutput(); err != nil {
		log.Error("run command: %v, err: %v, msg: %s", s, err, string(buf))
		return err
	}
	log.Debug("run command: %v, success", s)

	// Replace incorrect import path.
	f.replacePkgName()
	return nil
}

// For each included package, replace the incorrect import path generated by flatc with the correct one.
// For example, if file1.fbs includes a file and references a type in that file
// that is not in the same go package as file1.fbs,
// when flatc generates the stub code for file1.fbs,
// it will import the package using an import path that replaces dots in its namespace with slashes,
// which is not the format it should have (e.g. "trpc.group/..").
// Therefore, at this step,
// the generated import path needs to be replaced with the content of the go_package declared in the included file.
func (f *Fbs) replacePkgName() {
	curOutDir := path.Join(f.args.outDir, f.packagePath)
	for namespace, importPath := range f.pkg2ImportPath {
		originImportPath := strings.Replace(namespace, ".", "/", -1)
		s := "\"" + originImportPath + "\""
		files, err := filepath.Glob(path.Join(curOutDir, "*.go"))
		if err != nil {
			log.Debug("find .go files error: %v ", err)
			continue
		}
		_ = replacePkgNameInFiles(files, s, importPath)
	}
}

// replacePkgNameInFiles replaces the occurrences of originImport with importPath in the given list of files.
func replacePkgNameInFiles(files []string, originImport, importPath string) error {
	for _, file := range files {
		input, err := os.ReadFile(file)
		if err != nil {
			log.Debug("read file %v error: %v ", file, err)
			continue
		}
		lines := strings.Split(string(input), "\n")
		for i := range lines {
			lines[i] = strings.Replace(lines[i], originImport, "\""+importPath+"\"", -1)
		}
		output := strings.Join(lines, "\n")
		err = os.WriteFile(file, []byte(output), 0644)
		if err != nil {
			log.Debug("write file %v error: %v ", file, err)
			continue
		}
	}
	return nil
}
